﻿namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
    using System;
    using System.Diagnostics;
    using System.Diagnostics.Contracts;
    using System.Globalization;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Text;

    /// <summary>
    /// A utility class for doing MSTest Asserts with lambda expressions.
    /// </summary>
    /// <remarks>
    /// I saw this in MbUnit and decided to try to build it myself. It turned out to be very simple. MbUnit's version is much
    /// better but isn't integrated with MSTest.
    /// </remarks>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Justification = "I just like AssertEx instead of Assert2.")]
    [DebuggerStepThrough]
    public static class AssertEx
    {
        #region Public Methods
        /// <summary>
        /// Indicates that the assertion cannot be verified.
        /// </summary>
        public static void Inconclusive()
        {
            Assert.Inconclusive();
        }

        /// <summary>
        /// Indicates that the assertion can not be verified. Displays a message.
        /// </summary>
        /// <param name="message">A message to display. This message can be seen in the unit test results.</param>
        public static void Inconclusive(string message)
        {
            Assert.Inconclusive(message);
        }

        /// <summary>
        /// Indicates that an assertion can not be verified. Displays a message, and applies the specified formatting to it.
        /// </summary>
        /// <param name="message">A message to display. This message can be seen in the unit test results.</param>
        /// <param name="parameters">An array of parameters to use when formatting message.</param>
        public static void Inconclusive(string message, params object[] parameters)
        {
            Assert.Inconclusive(message, parameters);
        }

        /// <summary>
        /// Indicates that the assertion cannot be verified.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By design.")]
        public static void Inconclusive(Expression<Func<bool>> assertion)
        {
            Contract.Requires(assertion != null);

            AssertEx.Inconclusive(assertion, null);
        }

        /// <summary>
        /// Indicates that the assertion can not be verified. Displays a message.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        /// <param name="message">A message to display. This message can be seen in the unit test results.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By design.")]
        public static void Inconclusive(Expression<Func<bool>> assertion, string message)
        {
            Contract.Requires(assertion != null);

            AssertEx.ProcessAssertionExpression(
                assertion,
                assertMessage => AssertEx.BuildExceptionMessage(Properties.Resources.AssertExInconclusive, assertMessage, message),
                fullMessage => new AssertInconclusiveException(fullMessage));
        }

        /// <summary>
        /// Indicates that an assertion can not be verified. Displays a message, and applies the specified formatting to it.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        /// <param name="message">A message to display. This message can be seen in the unit test results.</param>
        /// <param name="parameters">An array of parameters to use when formatting message.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "By design.")]
        public static void Inconclusive(Expression<Func<bool>> assertion, string message, params object[] parameters)
        {
            Contract.Requires(assertion != null);

            AssertEx.Inconclusive(assertion, AssertEx.BuildFormattedMessage(message, parameters));
        }

        /// <summary>
        /// Asserts that the specified expression is true.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Sorry, need it this way.")]
        public static void That(Expression<Func<bool>> assertion)
        {
            Contract.Requires(assertion != null);

            AssertEx.That(assertion, null);
        }

        /// <summary>
        /// Asserts that the specified expression is true.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        /// <param name="message">The message to use if the expression is not <c>true</c>.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "ignore", Justification = "Ignoring the result.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Sorry, need it this way.")]
        public static void That(Expression<Func<bool>> assertion, string message)
        {
            Contract.Requires(assertion != null);

            AssertEx.ProcessAssertionExpression(
                assertion,
                assertMessage => AssertEx.BuildExceptionMessage(Properties.Resources.AssertExFailed, assertMessage, message),
                fullMessage => new AssertFailedException(fullMessage));
        }

        /// <summary>
        /// Asserts that the specified expression is true.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        /// <param name="message">The message to use if the expression is not <c>true</c>.</param>
        /// <param name="parameters">The parameters, if any, to use in the failure message.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Sorry, need it this way.")]
        public static void That(Expression<Func<bool>> assertion, string message, params object[] parameters)
        {
            Contract.Requires(assertion != null);

            AssertEx.That(assertion, AssertEx.BuildFormattedMessage(message, parameters));
        }

        /// <summary>
        /// Asserts that the expected exception is thrown.
        /// </summary>
        /// <typeparam name="TException">The type of the exception.</typeparam>
        /// <param name="action">The action that will cause the exception to be thrown.</param>
        /// <returns>The actual exception.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Nope, works better this way.")]
        public static TException Throws<TException>(Action action)
            where TException : Exception
        {
            if (action == null)
            {
                throw new ArgumentNullException("action");
            }
            Contract.EndContractBlock();

            try
            {
                action();
            }
            catch (Exception ex)
            {
                TException typedException = ex as TException;
                if (typedException != null)
                {
                    return typedException;
                }
                else
                {
                    throw new AssertFailedException(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Properties.Resources.WrongExceptionTypeThrown,
                            typeof(TException).FullName,
                            ex.GetType().FullName));
                }
            }

            throw new AssertFailedException(
                string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.ExpectedExceptionToBeThrown,
                    typeof(TException).FullName));
        }

        /// <summary>
        /// Asserts that the expected exception is thrown.
        /// </summary>
        /// <typeparam name="TException">The type of the exception.</typeparam>
        /// <param name="action">The action that will cause the exception to be thrown.</param>
        /// <param name="exceptionHandler">The exception handler.</param>
        public static void Throws<TException>(Action action, Action<TException> exceptionHandler)
            where TException : Exception
        {
            if (action == null)
            {
                throw new ArgumentNullException("action");
            }

            if (exceptionHandler == null)
            {
                throw new ArgumentNullException("exceptionHandler");
            }

            Contract.EndContractBlock();

            TException exception = AssertEx.Throws<TException>(action);
            exceptionHandler(exception);
        }

        /// <summary>
        /// Asserts that the expected exception is thrown.
        /// </summary>
        /// <typeparam name="TException">The type of the exception.</typeparam>
        /// <param name="action">The action that will cause the exception to be thrown.</param>
        /// <param name="expectedMessage">The expected message.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Nope, works better this way.")]
        public static void Throws<TException>(Action action, string expectedMessage)
            where TException : Exception
        {
            Contract.Requires(action != null);

            TException exception = AssertEx.Throws<TException>(action);
            AssertEx.That(() => exception.Message == expectedMessage);
        }
        #endregion Public Methods

        #region Private Methods
        private static string BuildExceptionMessage(string preMessage, string assertMessage, string message)
        {
            if (string.IsNullOrEmpty(message))
            {
                return string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.FailedWithoutMessage,
                    preMessage,
                    assertMessage);
            }
            else
            {
                return string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.FailedWithMessage,
                    preMessage,
                    assertMessage,
                    message);
            }
        }

        private static string BuildExpressionDescription(Expression expression)
        {
            Contract.Requires(expression != null);
            Contract.Ensures(Contract.Result<string>() != null);

            // If this is a Convert expression then use what is inside.
            Expression expressionToUse = expression;
            if (expression.NodeType == ExpressionType.Convert)
            {
                expressionToUse = ((UnaryExpression)expressionToUse).Operand;
            }

            if (expressionToUse.NodeType == ExpressionType.Call)
            {
                MethodCallExpression call = (MethodCallExpression)expressionToUse;
                return string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.MethodExpressionDescription,
                    AssertEx.BuildMethodDescription(call.Method));
            }

            if (expressionToUse.NodeType == ExpressionType.Constant)
            {
                // It doesn't make much sense to test a constant, but whatever...
                ConstantExpression constant = (ConstantExpression)expressionToUse;

                // Get the descriptive name of the constant.
                object constantValue;
                if (constant.Value == null)
                {
                    constantValue = Properties.Resources.NullText;
                }
                else
                {
                    constantValue = constant.Value;
                }

                return string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.ConstantExpressionDescription,
                    constantValue);
            }

            if (expressionToUse.NodeType == ExpressionType.MemberAccess)
            {
                MemberExpression memberExpression = (MemberExpression)expressionToUse;
                return string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.MemberAccessExpressionDescription,
                    memberExpression.Member.Name);
            }

            return Properties.Resources.UnknownExpressionDescription;
        }

        private static string BuildFormattedMessage(string message, params object[] parameters)
        {
            if (string.IsNullOrEmpty(message))
            {
                return null;
            }
            else
            {
                if ((parameters == null) || (parameters.Length == 0))
                {
                    return AssertEx.ReplaceNulls(message);
                }
                else
                {
                    return string.Format(CultureInfo.CurrentCulture, AssertEx.ReplaceNulls(message), parameters);
                }
            }
        }

        private static string BuildMethodDescription(MethodInfo method)
        {
            Contract.Requires(method != null);

            var builder = new StringBuilder();
            builder.Append(method.Name);
            builder.Append('(');

            bool needComma = false;
            foreach (var parameter in method.GetParameters())
            {
                if (needComma)
                {
                    builder.Append(", ");
                }
                else
                {
                    needComma = true;
                }

                if (parameter.IsOut)
                {
                    builder.Append("out ");
                }

                if (parameter.IsRetval)
                {
                    builder.Append("ref ");
                }

                builder.Append(parameter.Name);
            }

            builder.Append(')');

            return builder.ToString();
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "EndsWith", Justification = "That's the name of the method.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1307:SpecifyStringComparison", MessageId = "System.String.EndsWith(System.String)", Justification = "Have to run the exact method call.")]
        private static bool EvaluateStringEndsWith(MethodCallExpression methodCallExpression, string value, string substring)
        {
            Contract.Requires(methodCallExpression != null);
            Contract.Requires(value != null);

            if (substring == null)
            {
                throw new ArgumentNullException(substring);
            }

            switch (methodCallExpression.Arguments.Count)
            {
                case 1:
                    return value.EndsWith(substring);

                case 2:
                    StringComparison stringComparison = (StringComparison)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[1]);
                    return value.EndsWith(substring, stringComparison);

                case 3:
                    bool ignoreCase = (bool)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[1]);
                    CultureInfo culture = (CultureInfo)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[2]);
                    return value.EndsWith(substring, ignoreCase, culture);

                default:
                    throw new InvalidOperationException("Somehow String.EndsWith doesn't have the correct number of parameters.");
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "StartsWith", Justification = "That's the name of the method.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1307:SpecifyStringComparison", MessageId = "System.String.StartsWith(System.String)", Justification = "Have to run the exact method call.")]
        private static bool EvaluateStringStartsWith(MethodCallExpression methodCallExpression, string value, string substring)
        {
            Contract.Requires(methodCallExpression != null);
            Contract.Requires(value != null);

            if (substring == null)
            {
                throw new ArgumentNullException(substring);
            }

            switch (methodCallExpression.Arguments.Count)
            {
                case 1:
                    return value.StartsWith(substring);

                case 2:
                    StringComparison stringComparison = (StringComparison)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[1]);
                    return value.StartsWith(substring, stringComparison);

                case 3:
                    bool ignoreCase = (bool)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[1]);
                    CultureInfo culture = (CultureInfo)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[2]);
                    return value.StartsWith(substring, ignoreCase, culture);

                default:
                    throw new InvalidOperationException("Somehow String.StartsWith doesn't have the correct number of parameters.");
            }
        }

        private static object GetValueFromExpression(Expression expression)
        {
            if (expression == null)
            {
                throw new ArgumentNullException("expression");
            }

            // If this is a constant than we don't need to compile and execute. Just return the value.
            ConstantExpression constantExpression = expression as ConstantExpression;
            if (constantExpression != null)
            {
                return constantExpression.Value;
            }

            // If the expression is a class (not a struct) then just compile it. Otherwise we need to convert the
            // expression's value to an object.
            Expression expressionToReturn;
            if (expression.Type.IsClass)
            {
                expressionToReturn = expression;
            }
            else
            {
                expressionToReturn = Expression.Convert(expression, typeof(object));
            }

            // Compile, execute and return the value.
            var compiledLambda = Expression.Lambda<Func<object>>(expressionToReturn).Compile();
            return compiledLambda();
        }

        /// <summary>
        /// Some expressions work better if they are pre-processed.
        /// </summary>
        /// <param name="leftInExpression">The left in expression.</param>
        /// <param name="rightInExpression">The right in expression.</param>
        /// <param name="leftOutExpression">The left out expression.</param>
        /// <param name="rightOutExpression">The right out expression.</param>
        /// <remarks>
        /// Currently only make enum comparisons better.
        /// </remarks>
        private static void PreProcessExpressions(
            Expression leftInExpression,
            Expression rightInExpression,
            out Expression leftOutExpression,
            out Expression rightOutExpression)
        {
            Contract.Requires(leftInExpression != null);
            Contract.Requires(rightInExpression != null);
            Contract.Ensures(Contract.ValueAtReturn<Expression>(out leftOutExpression) != null);
            Contract.Ensures(Contract.ValueAtReturn<Expression>(out rightOutExpression) != null);

            // If the left side is an enum then convert from comparing integers to comparing the enum values.
            if (leftInExpression.NodeType == ExpressionType.Convert)
            {
                Expression operand = ((UnaryExpression)leftInExpression).Operand;
                if (operand.Type.IsEnum)
                {
                    leftOutExpression = operand;
                    rightOutExpression = Expression.Convert(rightInExpression, operand.Type);

                    return;
                }
            }

            // Just use the expressions as they are.
            leftOutExpression = leftInExpression;
            rightOutExpression = rightInExpression;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "ignore", Justification = "I like the code layout better this way.")]
        private static void ProcessAssertionExpression(
            Expression<Func<bool>> assertion,
            Func<string, string> buildFullMessage,
            Func<string, UnitTestAssertException> exceptionInstanceCreator)
        {
            Contract.Requires(assertion != null);

            Func<string, UnitTestAssertException> assertionBuilder =
                assertMessage => exceptionInstanceCreator(buildFullMessage(assertMessage));

            Contract.Assert(assertionBuilder != null);

            // Try each kind of exression until we find one that will work. Fall back to just evaluating the expression.
            var ignore = AssertEx.TryBinaryExpression(assertion.Body as BinaryExpression, assertionBuilder)
                || AssertEx.TryTypeBinaryExpression(assertion.Body as TypeBinaryExpression, assertionBuilder)
                || AssertEx.TryUnaryExpression(assertion.Body as UnaryExpression, assertionBuilder)
                || AssertEx.TryMethodCallExpression(assertion.Body as MethodCallExpression, assertionBuilder)
                || AssertEx.TryFallback(assertion, assertionBuilder);
        }

        /// <summary>
        /// Copied and modified from Assert.ReplaceNulls internal method.
        /// </summary>
        /// <param name="input">The input to replace nulls in.</param>
        /// <returns>The input without nulls.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Contracts", "Ensures", Justification = "Assert.ReplaceNullChars() doesn't return null.")]
        private static string ReplaceNulls(object input)
        {
            Contract.Ensures(Contract.Result<string>() != null);

            // Handle null input.
            if (input == null)
            {
                return Properties.Resources.NullText;
            }

            // Handle object that has a ToString of null.
            string stringRepresentation = input.ToString();
            if (stringRepresentation == null)
            {
                return Properties.Resources.ObjectText;
            }

            return Assert.ReplaceNullChars(stringRepresentation);
        }

        /// <summary>
        /// Tries expression of the type "x == y" and "x != y". Handles the special case of "x == null", "x != null", "x == true" and "x == false".
        /// </summary>
        /// <param name="binaryExpression">The binary expression.</param>
        /// <param name="assertionBuilder">The assertion builder.</param>
        /// <returns><c>true</c> if the expression was handled.</returns>
        private static bool TryBinaryExpression(
            BinaryExpression binaryExpression,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(assertionBuilder != null);

            // If this is a binary expression then use the approriate assertion, if any.
            if (binaryExpression != null)
            {
                // Handle comparisons to null. Note we are only checking the right side because that is the "expected" value.
                ConstantExpression rightConstant = binaryExpression.Right as ConstantExpression;
                if ((rightConstant != null) && (rightConstant.Value == null))
                {
                    object leftValue = AssertEx.GetValueFromExpression(binaryExpression.Left);

                    switch (binaryExpression.NodeType)
                    {
                        case ExpressionType.Equal:
                            if (leftValue != null)
                            {
                                throw assertionBuilder(
                                    AssertEx.BuildFormattedMessage(
                                        Properties.Resources.NullExpected,
                                        AssertEx.BuildExpressionDescription(binaryExpression.Left)));
                            }

                            return true;

                        case ExpressionType.NotEqual:
                            if (leftValue == null)
                            {
                                throw assertionBuilder(
                                    AssertEx.BuildFormattedMessage(
                                        Properties.Resources.NotNullExpected,
                                        AssertEx.BuildExpressionDescription(binaryExpression.Left)));
                            }

                            return true;
                    }
                }
                else if ((rightConstant != null) && (rightConstant.Value is bool))
                {
                    bool leftValue = (bool)AssertEx.GetValueFromExpression(binaryExpression.Left);
                    bool rightValue = (bool)rightConstant.Value;

                    switch (binaryExpression.NodeType)
                    {
                        case ExpressionType.Equal:
                            if (leftValue != rightValue)
                            {
                                string textToUse;
                                if (rightValue)
                                {
                                    textToUse = Properties.Resources.ExpectedTrue;
                                }
                                else
                                {
                                    textToUse = Properties.Resources.ExpectedFalse;
                                }

                                throw assertionBuilder(
                                    AssertEx.BuildFormattedMessage(
                                        textToUse,
                                        AssertEx.BuildExpressionDescription(binaryExpression.Left)));
                            }

                            return true;

                        case ExpressionType.NotEqual:
                            if (leftValue == rightValue)
                            {
                                string textToUse;
                                if (rightValue)
                                {
                                    textToUse = Properties.Resources.ExpectedFalse;
                                }
                                else
                                {
                                    textToUse = Properties.Resources.ExpectedTrue;
                                }

                                throw assertionBuilder(
                                    AssertEx.BuildFormattedMessage(
                                        textToUse,
                                        AssertEx.BuildExpressionDescription(binaryExpression.Left)));
                            }

                            return true;
                    }
                }
                else
                {
                    // Some comparisons work better if the expressions are modified a bit...
                    Expression leftExpression;
                    Expression rightExpression;
                    AssertEx.PreProcessExpressions(
                        binaryExpression.Left,
                        binaryExpression.Right,
                        out leftExpression,
                        out rightExpression);

                    object actualValue = AssertEx.GetValueFromExpression(leftExpression);
                    object expectedValue = AssertEx.GetValueFromExpression(rightExpression);

                    switch (binaryExpression.NodeType)
                    {
                        case ExpressionType.Equal:
                            if (!object.Equals(actualValue, expectedValue))
                            {
                                throw assertionBuilder(
                                    AssertEx.BuildFormattedMessage(
                                        Properties.Resources.EqualExpected,
                                        AssertEx.BuildExpressionDescription(leftExpression),
                                        expectedValue,
                                        actualValue));
                            }

                            return true;

                        case ExpressionType.NotEqual:
                            if (object.Equals(actualValue, expectedValue))
                            {
                                throw assertionBuilder(
                                    AssertEx.BuildFormattedMessage(
                                        Properties.Resources.NotEqualExpected,
                                        AssertEx.BuildExpressionDescription(leftExpression),
                                        expectedValue,
                                        actualValue));
                            }

                            return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Checks if the assertion is true.
        /// </summary>
        /// <param name="assertion">The assertion.</param>
        /// <param name="assertionBuilder">The assertion builder.</param>
        /// <returns>Always <c>true</c>.</returns>
        private static bool TryFallback(
            Expression<Func<bool>> assertion,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(assertion != null);
            Contract.Requires(assertionBuilder != null);

            var value = assertion.Compile()();
            if (!value)
            {
                throw assertionBuilder(
                    AssertEx.BuildFormattedMessage(
                        Properties.Resources.ExpectedTrue,
                        AssertEx.BuildExpressionDescription(assertion.Body)));
            }

            return true;
        }

        /// <summary>
        /// Tries expressions of type "string".StartsWith("xxx"), "string".EndsWith("xxx") and object.ReferenceEquals(x, y).
        /// </summary>
        /// <param name="methodCallExpression">The method call expression.</param>
        /// <param name="assertionBuilder">The assertion builder.</param>
        /// <returns><c>true</c> if the expression was handled.</returns>
        private static bool TryMethodCallExpression(
            MethodCallExpression methodCallExpression,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(assertionBuilder != null);

            if (methodCallExpression != null)
            {
                if (methodCallExpression.Method.DeclaringType == typeof(string))
                {
                    switch (methodCallExpression.Method.Name)
                    {
                        case "EndsWith":
                            return TryStringEndsWithExpression(methodCallExpression, assertionBuilder);

                        case "StartsWith":
                            return TryStringStartsWithExpression(methodCallExpression, assertionBuilder);
                    }
                }
                else if (IsObjectReferenceEquals(methodCallExpression))
                {
                    return TryObjectReferenceEqualsExpression(
                        methodCallExpression,
                        assertionBuilder,
                        checkForEquals: true);
                }
            }

            return false;
        }

        private static bool IsObjectReferenceEquals(MethodCallExpression methodCallExpression)
        {
            Contract.Requires(methodCallExpression != null);

            return (methodCallExpression.Method.DeclaringType == typeof(object))
                && (methodCallExpression.Method.Name == "ReferenceEquals");
        }

        private static bool TryObjectReferenceEqualsExpression(
            MethodCallExpression methodCallExpression,
            Func<string, UnitTestAssertException> assertionBuilder,
            bool checkForEquals)
        {
            Contract.Requires(methodCallExpression != null);
            Contract.Requires(assertionBuilder != null);
            Contract.Assume(methodCallExpression.Arguments.Count == 2);
            Contract.Assume(methodCallExpression.Arguments[0] != null);
            Contract.Assume(methodCallExpression.Arguments[1] != null);

            object parameter1 = AssertEx.GetValueFromExpression(methodCallExpression.Arguments[0]);
            object parameter2 = AssertEx.GetValueFromExpression(methodCallExpression.Arguments[1]);
            if (object.ReferenceEquals(parameter1, parameter2) != checkForEquals)
            {
                throw assertionBuilder(
                    AssertEx.BuildFormattedMessage(
                        checkForEquals ? Properties.Resources.NotSameInstance : Properties.Resources.SameInstance,
                        AssertEx.BuildExpressionDescription(methodCallExpression.Arguments[0]),
                        AssertEx.BuildExpressionDescription(methodCallExpression.Arguments[1])));
            }

            return true;
        }

        private static bool TryStringEndsWithExpression(
            MethodCallExpression methodCallExpression,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(methodCallExpression != null);
            Contract.Requires(assertionBuilder != null);
            Contract.Assume(methodCallExpression.Object != null);

            var value = (string)AssertEx.GetValueFromExpression(methodCallExpression.Object);
            if (value == null)
            {
                throw assertionBuilder(
                    AssertEx.BuildFormattedMessage(
                        Properties.Resources.TargetValueIsNull,
                        AssertEx.BuildExpressionDescription(methodCallExpression.Object)));
            }

            Contract.Assume(methodCallExpression.Arguments.Count > 0);
            var substring = (string)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[0]);

            if (!AssertEx.EvaluateStringEndsWith(methodCallExpression, value, substring))
            {
                Contract.Assume(methodCallExpression.Object != null);
                throw assertionBuilder(
                    AssertEx.BuildFormattedMessage(
                        Properties.Resources.StringEndsWithFailed,
                        AssertEx.BuildExpressionDescription(methodCallExpression.Object),
                        value,
                        substring));
            }

            return true;
        }

        private static bool TryStringStartsWithExpression(
            MethodCallExpression methodCallExpression,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(methodCallExpression != null);
            Contract.Requires(assertionBuilder != null);
            Contract.Assume(methodCallExpression.Object != null);

            var value = (string)AssertEx.GetValueFromExpression(methodCallExpression.Object);
            if (value == null)
            {
                throw assertionBuilder(
                    AssertEx.BuildFormattedMessage(
                        Properties.Resources.TargetValueIsNull,
                        AssertEx.BuildExpressionDescription(methodCallExpression.Object)));
            }

            Contract.Assume(methodCallExpression.Arguments.Count > 0);
            var substring = (string)AssertEx.GetValueFromExpression(methodCallExpression.Arguments[0]);

            if (!AssertEx.EvaluateStringStartsWith(methodCallExpression, value, substring))
            {
                Contract.Assume(methodCallExpression.Object != null);
                throw assertionBuilder(
                    AssertEx.BuildFormattedMessage(
                        Properties.Resources.StringStartsWithFailed,
                        AssertEx.BuildExpressionDescription(methodCallExpression.Object),
                        value,
                        substring));
            }

            return true;
        }

        /// <summary>
        /// Tries expression of the type "x is y".
        /// </summary>
        /// <param name="typeBinaryExpression">The type binary expression.</param>
        /// <param name="assertionBuilder">The assertion builder.</param>
        /// <returns><c>true</c> if the expression was handled.</returns>
        private static bool TryTypeBinaryExpression(
            TypeBinaryExpression typeBinaryExpression,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(assertionBuilder != null);

            // Handle x is y.
            if (typeBinaryExpression != null)
            {
                object value = AssertEx.GetValueFromExpression(typeBinaryExpression.Expression);
                if (value == null)
                {
                    throw assertionBuilder(
                        AssertEx.BuildFormattedMessage(
                            Properties.Resources.ExpectedTypeButWasNull,
                            AssertEx.BuildExpressionDescription(typeBinaryExpression.Expression),
                            typeBinaryExpression.TypeOperand.FullName));
                }
                else if (!typeBinaryExpression.TypeOperand.IsInstanceOfType(value))
                {
                    throw assertionBuilder(
                        AssertEx.BuildFormattedMessage(
                            Properties.Resources.ExpectedDifferentType,
                            AssertEx.BuildExpressionDescription(typeBinaryExpression.Expression),
                            typeBinaryExpression.TypeOperand.FullName,
                            value.GetType().FullName));
                }

                return true;
            }

            return false;
        }

        /// <summary>
        /// Tries expressions of type "!value" and "!(x is y)".
        /// </summary>
        /// <param name="unaryExpression">The unary expression.</param>
        /// <param name="assertionBuilder">The assertion builder.</param>
        /// <returns><c>true</c> if the expression was handled.</returns>
        private static bool TryUnaryExpression(
            UnaryExpression unaryExpression,
            Func<string, UnitTestAssertException> assertionBuilder)
        {
            Contract.Requires(assertionBuilder != null);

            if (unaryExpression != null)
            {
                if (unaryExpression.NodeType == ExpressionType.Not)
                {
                    // Handle "!(x is y)".
                    var typeBinaryExpression = unaryExpression.Operand as TypeBinaryExpression;
                    if (typeBinaryExpression != null)
                    {
                        object instance = AssertEx.GetValueFromExpression(typeBinaryExpression.Expression);
                        if (instance == null)
                        {
                            throw assertionBuilder(
                                AssertEx.BuildFormattedMessage(
                                    Properties.Resources.NotExpectedTypeButWasNull,
                                    AssertEx.BuildExpressionDescription(typeBinaryExpression.Expression),
                                    typeBinaryExpression.TypeOperand.FullName));
                        }
                        else if (typeBinaryExpression.TypeOperand == instance.GetType())
                        {
                            throw assertionBuilder(
                                AssertEx.BuildFormattedMessage(
                                    Properties.Resources.NotExpectedType,
                                    AssertEx.BuildExpressionDescription(typeBinaryExpression.Expression),
                                    typeBinaryExpression.TypeOperand.FullName,
                                    instance.GetType().FullName));
                        }
                        else if (typeBinaryExpression.TypeOperand.IsInstanceOfType(instance))
                        {
                            throw assertionBuilder(
                                AssertEx.BuildFormattedMessage(
                                    Properties.Resources.NotExpectedTypeSubClass,
                                    AssertEx.BuildExpressionDescription(typeBinaryExpression.Expression),
                                    typeBinaryExpression.TypeOperand.FullName,
                                    instance.GetType().FullName));
                        }

                        return true;
                    }

                    // Handle "!object.ReferenceEquals(o1, o2)".
                    MethodCallExpression methodCallExpression = unaryExpression.Operand as MethodCallExpression;
                    if (methodCallExpression != null)
                    {
                        if (IsObjectReferenceEquals(methodCallExpression))
                        {
                            return TryObjectReferenceEqualsExpression(
                                methodCallExpression,
                                assertionBuilder,
                                checkForEquals: false);
                        }
                    }

                    // Handle "!value".
                    bool value = Expression.Lambda<Func<bool>>(unaryExpression.Operand).Compile()();
                    if (value)
                    {
                        throw assertionBuilder(
                            AssertEx.BuildFormattedMessage(
                                Properties.Resources.ExpectedFalse,
                                AssertEx.BuildExpressionDescription(unaryExpression.Operand)));
                    }

                    return true;
                }
            }

            return false;
        }
        #endregion Private Methods
    }
}